查看原文
其他

我为什么能将效率提升了800%

郭霖 郭霖 2020-10-29

LitePal上一个版本发布时我还没有正式开始运营我的公众号,而现在经过一个多月的运营,我的公众号也是增加了很多的新朋友,那么可能有一部分新朋友并不知道LitePal是什么,这里我先做个简单的介绍吧。


LitePal是一个由我编写的Android端数据库ORM框架,极度简化了数据库的使用方式,我们甚至不用写一行SQL语句就能完成绝大部分的数据库操作,大大降低了开发的成本。LitePal一直维持着稳定的更新频率,目前已有多个版本迭代,而最新的1.3.2版本更是极大幅度地提升了效率。


自从我开始用心运营公众号之后,每篇文章我都希望大家能够学到些有用的知识,因此本篇文章我也不打算就仅仅是介绍一下LitePal有什么功能变更,而是想和大家讲一讲为什么我能将效率提升了这么大的幅度。


分析和解决性能问题


一切缘由都始于DDUP群的小庄,他准备在他们公司的项目中使用LitePal,由于公司比较大,正式使用之前需要先做测试评估,于是他对LitePal进行了一系列的测试,其中在压力测试方面发现了问题。


当时他的测试方法是对10000条数据进行存储和查询,使用LitePal存储耗时长达40.3秒,查询耗时长达22.1秒,而使用他们公司自己写的数据库框架存储同样多的数据耗时只有5秒多。虽说在手机端我很难想象有什么样的场景是需要一次性存储10000条数据的,但是面对这么尴尬的性能数据对比,我还是下定决心好好查一查影响性能的原因到底在哪里。


我平时查问题惯用的一招就是排除法,把一套流程执行下来的代码先看一遍,分析其中有哪些部分是可能产生问题的,然后开始将它们一行行进行注释,注释之后重新运行如果问题还在,那么说明不是这行代码造成的,继续注释下一行,如果问题不见了,那么恭喜,你已经找到问题的原因了。


这次查找LitePal的性能问题我也是使用的同样的方法,一开始我怀疑是表关联的原因造成的性能影响,我把所有关联关系的代码全注释掉之后,发现性能只提升了200毫秒,看来问题不在这里。后面我继续使用排除法一层层查找,结果发现如果将反射取值的代码注释掉,整个10000条数据的存储时间只需1.8秒,看来问题的根源就是这儿了。


可是ORM框架的核心就是利用反射将对象中的值取出,然后存入到数据库中,不用反射的话任何数据库框架都无法正常工作的,但是为何小庄他们公司的数据库框架性能却很好呢?我让他截了部分代码给我分析,结果发现在使用反射取值的方式上有区别。


我们来看这样一个实体类:


public class Person {
   private String name;
       
   public String getName() {
       return name;    }
           
   public void setName(String name) {
       this.name = name;    } }


Person类中有一个name字段,还提供了name字段对应的getter、setter方法。这种写法在面向对象编程当中叫作封装,表示我们通常不将类中的字段直接暴露给外部,而是提供一些public方法来允许外部访问。LitePal在设计的时候希望所有开发者都能按照面向对象的编程思想将实体类进行封装,因此在内部LitePal是通过getName()方法来获取name值的,则小庄他们公司的框架则是直接通过name字段来获取值的。


区别就在这里。


为此我专门做了一系列的测试来对比反射调用方法和反射获取字段的性能差异,结果发现在PC上并没有明显的区别,但在手机上则差异巨大。我修改了LitePal的内部反射机制,改成了使用字段来取值,结果存储10000条数据只需4.7秒,查询10000条数据只需3.4秒,性能提升了800%以上!


另外我又使用了多个手机进行性能对比,发现每台手机对于反射调用方法和反射调用字段的效率差异方面还都不同,其中以nexus 6p的差异最大,下表是详细的时间对比:


存储10000条数据耗时反射调用方法取值反射调用字段取值
小米 540.3 s4.7 s
Nexus 558.5 s8.7 s
Nexus 6p84.1 s3.8 s


可以看到,在nexus 6p上,效率甚至提升了22倍!面对如此巨大的性能对比,我不得不在最新的LitePal 1.3.2版本中,将反射取值的方式改成了调用字段来获取。当然这些修改都是内部机制修改,不需要大家做任何代码变动,只需升级一下版本就能享受到巨大的性能提升了。


当然,1.3.2版本并不仅仅是提升了性能而已,我们再来看一下还有哪些其他的新功能。


支持外部存储


有不少朋友问过我,LitePal能不能将数据库文件存放在SD卡上?其实对于这样的需求我感到很奇怪,像数据库这么隐私的文件,怎么能够存放在SD卡上,让任何人都能看得到呢?


但也有另外一部分朋友问我,使用LitePal存储了数据,但是手机没有root,怎么能够直观地看出数据有没有存储成功呢?


确实,看来在开发阶段调试的时候,将数据库文件存放在SD卡还是挺有用的,于是我在1.3.2版本中加入了这一功能。配置也很简单,只需要在litepal.xml中添加如下配置即可:


<?xml version="1.0" encoding="utf-8"?> <litepal>  ......  <storage value="external"></storage> </litepal>


加入这一行配置之后,数据库文件会被自动创建在/sdcard/Android/data/<package name>/files/databases目录下,我们使用Root Explorer就能直接查看到里面的数据了,如下图所示:



注意,此功能尽量只在调试的时候使用,把数据库文件存放在SD卡真的很不安全。


支持继承属性


之前LitePal要求所有的实例类都是必须继承自DataSupport类的,但有不少朋友反馈,他们的实体类本身就是有继承结构的,比如下面这种情况:


public class Student extends Person {
   private int grade;
   
   public int getGrade() {
       return grade;    }
       
   public void setGrade(int grade) {
       this.grade = grade;    } }


Student类继承自Person类,因为Student是属于Person的一种,然后Person类再继承自DataSupport,如下所示:


public class Person extends DataSupport {
   private String name;
   
   public String getName() {        
       return name;    }    
   
   public void setName(String name) {
       this.name = name;    } }


这种写法其实很正常,但是很可惜之前的LitePal没办法处理这种写法,因为使用反射无法在Student对象中拿到Person类中的字段,因此你会发现student表中只会有grade这一列。


而1.3.2版本中LitePal则对这方面进行了改进,使用递归读取的方式将所有父类中声明的全部字段一层层找到,直到读取到DataSupport类就跳出递归。


更新后再使用同样的写法,你会发现student表中就拥有grade和name这两列了。


更多API


除此之外,1.3.2版本中还加入了几个非常有用的API,让我们的数据库操作更加便利。


比如在级联查询中加入了findFirst()和findLast()方法,如果我们要查询名字叫Tom的第一个学生,就可以这样写:


Student tom = DataSupport.where("name = ?", "Tom").findFirst(Student.class);


比起之前要先得到一个结果集List,然后再从List当中获取第一条数据,findFirst()方法明显让代码更简单了。


另外,如果想要判断某条数据存不存在,之前也是没有特别简便的方法的,而新增的isExist()方法则很好地解决了这个问题。比如我们想要查询有没有名字叫Jimmy的学生,就可以这样写:


if (DataSupport.isExist(Student.class, "name = ?", "Jimmy")) {
 // 存在名叫Jimmy的学生
} else {
 // 不存在名叫Jimmy的学生
}


然后在isExist()方法的基础之上,新版本还加入了一个saveIfNotExist(),表示只有当目标数据不存在的时候才将数据存入到数据库。这个方法在要求数据唯一性的时候非常有用,比如user表中要求用户名必须唯一,那么就可以这样写:


User user = new User(); user.setUsername("Tom"); user.setPassword("123456") user.saveIfNotExist("username = ?", "Tom")


上述代码的意思就是,只有在user表中没有Tom这个username的时候才会存储这条数据并返回true,否则就会认为数据已经存在了,从而忽略这次存储操作,并且saveIfNotExist()方法返回false。


如何升级


升级方式一如既往的简单,如果你使用的是Android Studio,只需要在build.gradle中修改一下配置即可:


dependencies {    compile 'org.litepal.android:core:1.3.2'
}


1.3.2版本中的所有的功能都是向下兼容的,因此你的升级不用付出任何成本。


如果你使用的还是Eclipse,那么就需要到LitePal的项目主页去下载最新版的jar包了,项目主页地址是:



另外,对于那些之前没有了解过LitePal,但是想从头去学习的朋友们,可以去参考我之前写过的一个专栏,里面非常详细地介绍了LitePal从零到精通的所有用法,点击下方 阅读原文 将会直接跳转到LitePal的专栏页面。




如果你有好的技术文章想和大家分享,欢迎向我的公众号投稿,投稿具体细节请在公众号主页点击“投稿”菜单查看。


欢迎长按下图 -> 识别图中二维码或者扫一扫关注我的公众号:

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存